﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using NetworkLib;
using System.Net;
using System.Threading;
using System.Timers;
using SettLib;
using DatabaseLib;

// SysLog Server Class v.1.0
// by George Makarov
// Email: georgemakarov1@yandex.ru


namespace SysLogLib
{
    public class cSysLog
    {

        private class cSyslogCache                  //класс кэша запросов
        {
            public int CacheIndex;                  //позиция указателя в кэше
            public string[] Cache;                  //кэш
            private int CacheSize;                  //размер кэша
            public bool Flushing;

            //конструктор
            public cSyslogCache(int pCacheSize)
            {
                CacheSize = pCacheSize;
                Cache = new string[CacheSize];
            }

            //обнуление индекса
            public void ResetCacheIndex()
            {
                CacheIndex = 0;
            }          

            //проверка на заполненность кэша
            public bool CacheIsFull()
            {
                return CacheIndex == CacheSize;
            }

            //добавление запроса в кэш
            public void AddSyslogQuery(string Query)
            {
                Cache[CacheIndex] = Query;
                CacheIndex++;
            }
        }
        private struct SysLogEvent                  //структура syslog-сообщения
        {
            public int Facility;                    //Объект, сгенерировавший сообщение
            public int Severity;                    //Уточнение типа сообщения
            public DateTime DTStamp;                //временая отметка сообщения
            public IPAddress Src;                   //ip-адрес источника
            public IPAddress Dst;                   //ip-адрес назначения
            public int Received;                    //получено
            public int Transferred;                 //передано
            public string Recvif;                   //входящий интерфейс
            public string Destif;                   //исходящий интерфейс
            public string Action;                   //действие над пакетом
            public int IPDatalen;                   //длина пакета в случае его отброса
            //public string Event;
            public string Proto;                    //используемый протокол
        }
        private List<cSyslogCache> lSyslogCache;    //список кэшей 
        private Queue<string> SysLogEventsQueue;    //очередь пришедших сообщений syslog
        private DataTable dtIPAddresses;            //служебная таблица IP-адресов
        private DataTable dtInterfaces;             //служебная таблица интерфейсов
        private DataTable dtActions;                //служебная таблица значений action
        private DataTable dtProtos;                 //служебная таблица протоколов
        private int IPAddressesSaved;               //кол-во сохраненных ip-адресов
        private int InterfacesSaved;                //кол-во сохраненных интерфейсов
        private int ProtosSaved;                    //кол-во сохраненных протоколов
        private int ActionsSaved;                   //кол-во сохраненных значений action
        private byte CurrentCache = 0;              //текущий кэш
        private System.Timers.Timer FlushTimer;     //таймер сброса кэша записей в БД
        private System.Timers.Timer DnsResolverTimer;   //таймер разрешения DNS-имен       
        private cNetwork NetworkObj;                //объект для работы с сетью
        public delegate void dErrorHandler(Exception e);
        public event dErrorHandler ErrorHandler;    //обработчик ошибки
        public delegate void dLogHandler(string Msg);
        public event dLogHandler LogHandler;        //обработчик записи в лог
        
        private bool bRunning;                      //признак работы сервера
        public bool Running
        {
            get { return bRunning; }
        }


        private cSettings.cSyslogSett Settings;     //настройки сервера
        private DB db;


        //конструктор
        public cSysLog(cSettings.cSyslogSett pSett, DB pDB)
        {
            Settings = pSett;
            db = pDB;

            lSyslogCache = new List<cSyslogCache>();
            for (int i = 0; i < Settings.CachesCount; i++)
            {
                cSyslogCache SyslogCache = new cSyslogCache(Settings.CacheSize);
                lSyslogCache.Add(SyslogCache);
            }
            SysLogEventsQueue = new Queue<string>();
            FlushTimer = new System.Timers.Timer(Settings.FlushPeriod*1000);
            FlushTimer.Elapsed += FlushTimer_OnTick;
            DnsResolverTimer = new System.Timers.Timer(Settings.DNSResolvePeriod*1000);
            DnsResolverTimer.Elapsed += DnsResolverTimer_OnTick;
            NetworkObj = new cNetwork();
            NetworkObj.UDPReceiveHandler += OnUDPReceive;
            NetworkObj.ErrorHandler += delegate(Exception e)
            {
                ErrorHandler(e);
            };

        }

        //деструктор
        ~cSysLog()
        {
            if (bRunning)
                Stop();
        }

        //return 0 if no errors
        public int Start()
        {
            if (bRunning)
                return 2;
            //if ((db.GetQueryTable == null) || (ExecuteQuery == null) || (ExecuteQueryBatch == null))
            //    return 1;
            try
            {
                //получение служебных таблиц
                dtIPAddresses = db.GetQueryTable("Select * from TAddresses");
                dtInterfaces = db.GetQueryTable("select * from TInterfaces");
                dtActions = db.GetQueryTable("Select * from TActions");
                dtProtos = db.GetQueryTable("Select * from TProtos");
                IPAddressesSaved = dtIPAddresses.Rows.Count;
                InterfacesSaved = dtInterfaces.Rows.Count;
                ActionsSaved = dtActions.Rows.Count;
                ProtosSaved = dtProtos.Rows.Count;
            }
            catch (Exception ex)
            {
                if (ErrorHandler != null)
                    ErrorHandler(new Exception("SyslogServer : Ошибка при запросе служебных таблиц ", ex));
                return 3;
            }
            //запуск таймера сброса записей в БД
            FlushTimer.Start();
            //запуск таймера разрешения dns-имен
            DnsResolverTimer.Start();
            //запуск потока ожидания сообщения на порту
            NetworkObj.UDPWaitMessage_socket(Settings.Port);
            bRunning = true;
            //запуск потока, обрабатывающего очередь входящих сообщений
            Thread QueueServiceThread = new Thread(QueueService);
            QueueServiceThread.Start();
            if (LogHandler != null)
                LogHandler("SyslogServer started");
            return 0;
        }

        public void Stop()
        {
            if (bRunning)
            {
                //возможна потеря одного пакета, пришедшего уже после остановки
                NetworkObj.UDPStopWaitMessage();
                while (SysLogEventsQueue.Count > 0)
                { }
                //сброс записей в БД
                FlushTimer_OnTick(null, null);
                //разрешение последних пришедших dns-имен
                DnsResolverTimer_OnTick(null, null);
                bRunning = false;
                if (LogHandler != null)
                    LogHandler("SyslogServer stoppped");

            }
        }

        //обработчик очереди входящий сообщений
        private void QueueService()
        {
            //пока запущен сервер, обрабатываем очередь
            while (bRunning)
            {
                //пауза для освобождения ресурсов ЦП
                Thread.Sleep(Settings.EventsQueueProcessingPeriod);
                //если очередь пуста, то выходим
                if (SysLogEventsQueue.Count > 0)
                {
                    string EventString = SysLogEventsQueue.Dequeue();
                    //создаем новый объект сообщения syslog
                    SysLogEvent SLDetails = new SysLogEvent();
                    //если он нам подходит, то помещаем запрос, построенный на основе сообщения в кэш
                    if (ParseSysLogMessage(EventString, ref SLDetails))
                        SaveRecord(SLDetails);
                }
            }
        }

        //Only For Debug, in release must deleted
        #region Only For Debug, in release must deleted
        public int GetCurrentCacheSize()
        {
            return lSyslogCache[CurrentCache].CacheIndex; 
        }

        public int GetCurrentCacheNumber()
        {
            return CurrentCache;
        }

        public int GetQueueLength()
        {
            return SysLogEventsQueue.Count;
        }
        #endregion

        //получатель пакета по UDP
        private void OnUDPReceive(cNetwork.stUDPReceiveData RecieveData)
        {
            //Добавляем пришедшее сообщение в очередь
            SysLogEventsQueue.Enqueue(RecieveData.Data);
        }

        //получение ip адреса интерфейса по его имени из массива из настроек программы
        private string GetIfIPAddressByName(string IfName)
        {
            string Result = string.Empty;
            //перебираем интерфейсы
            foreach (object[] Interface in Settings.Interfaces)
            {
                //если равны имена, то возвращаем ip
                if (Interface[1].ToString().ToLower() == IfName.ToLower())
                {
                    Result = Interface[0].ToString();
                    break;
                }
            }
            return Result;
        }

        //добавление запроса, построенного на базе данных сообщения в кэш
        private void SaveRecord(SysLogEvent Data)
        {
            //получение идентификаторов ip-адресов источника и назначения по ip-адресу
            Guid SrcAddrID = DB.GetOrInsertID(dtIPAddresses, FieldTypes.u, "AddressID", new string[] { "IPAddress" }, new string[] { Data.Src.ToString() });
            Guid DstAddrID = DB.GetOrInsertID(dtIPAddresses, FieldTypes.u, "AddressID", new string[] { "IPAddress" }, new string[] { Data.Dst.ToString() });
            //получение идентификаторов входящиего и исходящего интерфейсов по их имени
            Guid IfIPAddressID = Guid.Empty;
            if (dtInterfaces.Select("Name = '" + Data.Recvif + "'").Length == 0)
            {
                //если такого интерфейса в базе нет, но пытаемся получить его ip из настроек программы
                string IfIPAddress = GetIfIPAddressByName(Data.Recvif);
                if (IfIPAddress != string.Empty)
                {
                    //если в настройках есть ip, то получаем новый или существующий uid этой записи
                    IfIPAddressID = DB.GetOrInsertID(dtIPAddresses, FieldTypes.u, "AddressID", new string[] { "IPAddress" }, new string[] { IfIPAddress });
                }
            }
            Guid RecvIfID;
            //если ip-адреса нет, то сохраняем без него
            if (IfIPAddressID == Guid.Empty)
                RecvIfID = DB.GetOrInsertID(dtInterfaces, FieldTypes.u, "InterfaceID", new string[] { "Name" }, new string[] { Data.Recvif });
            else
                RecvIfID = DB.GetOrInsertID(dtInterfaces, FieldTypes.u, "InterfaceID", new string[] { "Name", "AddressID" }, new string[] { Data.Recvif, IfIPAddressID.ToString() });
            IfIPAddressID = Guid.Empty;
            if (dtInterfaces.Select("Name = '" + Data.Destif + "'").Length == 0)
            {
                string IfIPAddress = GetIfIPAddressByName(Data.Destif);
                if (IfIPAddress != string.Empty)
                {
                    IfIPAddressID = DB.GetOrInsertID(dtIPAddresses, FieldTypes.s, "AddressID", new string[] { "IPAddress" }, new string[] { IfIPAddress });
                }
            }
            Guid DestIfID;
            if (IfIPAddressID == Guid.Empty)
                DestIfID = DB.GetOrInsertID(dtInterfaces, FieldTypes.u, "InterfaceID", new string[] { "Name" }, new string[] { Data.Destif });
            else
                DestIfID = DB.GetOrInsertID(dtInterfaces, FieldTypes.u, "InterfaceID", new string[] { "Name", "AddressID" }, new string[] { Data.Destif, IfIPAddressID.ToString() });
            //получение идентификатора действия и протокола
            Guid ProtoID = DB.GetOrInsertID(dtProtos, FieldTypes.u, "ProtoID", new string[] { "Name" }, new string[] { Data.Proto });
            Guid ActionID = DB.GetOrInsertID(dtActions, FieldTypes.u, "ActionID", new string[] { "Name" }, new string[] { Data.Action });

            //перебираем и сливаем кэши, пока не найдем незаполненный и не сливаемый кэш
            while (lSyslogCache[CurrentCache].CacheIsFull())
            {
                FlushTimer_OnTick(null, null);
            }

            string Fields = "LogID, SrcID, DstID, DTStamp, Received, Transferred, IPDataLen";
            string Values = "NEWID(),'" + SrcAddrID.ToString() + "','" + DstAddrID.ToString() + "','" +
                Data.DTStamp.ToString("yyyy-MM-dd HH:mm:ss") + "', " + Data.Received.ToString() + "," + 
                Data.Transferred.ToString() + "," + Data.IPDatalen.ToString();
            //если есть входящий интерфейс, то добавляем его в запрос
            if (RecvIfID != Guid.Empty)
            {
                Fields += ", RecvIfID";
                Values += ", '" + RecvIfID.ToString() + "'";
            }
            //если есть исходящий интерфейс, то добавляем его в запрос
            if (DestIfID != Guid.Empty)
            {
                Fields += ", DestIfID";
                Values += ", '" + DestIfID.ToString() + "'";
            }
            //если есть действие, то добавляем его в запрос
            if (ActionID != Guid.Empty)
            {
                Fields += ", ActionID";
                Values += ", '" + ActionID.ToString() + "'";
            }
            //если есть протокол, то добавляем его в запрос
            if (ProtoID != Guid.Empty)
            {
                Fields += ", ProtoID";
                Values += ", '" + ProtoID.ToString() + "'";
            }
            //добавляем в текущий кэш запрос
            lSyslogCache[CurrentCache].AddSyslogQuery("Insert into TLog (" + Fields + ")" +
                " values (" + Values + ")");           
        }

        //функция запускаемая в отдельном потоке для разрешения имени хоста по его ip
        private void DnsResolve()
        {
            //проверяем есть ли обработчики событий для запросов
            //if ((GetQueryTable != null) && (ExecuteQuery != null))
            //{
                //выбираем все записи, где имя хоста еще не получено
                DataTable DT = db.GetQueryTable("Select * from TAddresses where hostname is null");
                if (DT.Rows.Count > 0)
                {
                    foreach (DataRow DR in DT.Rows)
                    {
                        //берем ip адрес
                        string IPAddress = DR["IPAddress"].ToString();
                        //запрашиваем имя хоста по ip, на все даем время N сек (таймаут берем из настроек)
                        string HostName = cNetwork.GetReverseDNS(IPAddress, Settings.DNSResolveTimeout*1000);
                        db.ExecuteQuery("update Taddresses set hostname = '" + HostName + "' where addressid = '" + DR["AddressID"].ToString() + "'");
                    }
                }
                if (LogHandler != null)
                    LogHandler("Hostnames resolved: " + DT.Rows.Count.ToString());
                //запускаем снова таймер
                DnsResolverTimer.Start();
            //}
        }

        //обработчик события ontick таймер для резолвинга имени хоста по его ip
        private void DnsResolverTimer_OnTick(object sender, EventArgs e)
        {
            //останавливаем таймер
            DnsResolverTimer.Stop();
            //и запускаем резолвинг имени хоста в новом потоке
            Thread DnsResolveThread = new Thread(DnsResolve);
            DnsResolveThread.Start();
        }

        //сброс данных из текущего кэш в БД
        private void FlushTimer_OnTick(object sender, EventArgs e)
        {
            //переключаем указатель текущего кэша на следующий массив
            int OldUsedCache = CurrentCache;
            if (CurrentCache == Settings.CachesCount - 1)
                CurrentCache = 0;
            else
                CurrentCache++;
            //если кэш уже сливается, то выходим
            if (lSyslogCache[CurrentCache].Flushing)
                return;
            //выставляем метку, что кэш сливается, что туда не начали запись
            lSyslogCache[CurrentCache].Flushing = true;
            //останавливаем таймер, что б не было попыток одновременных сливов, если это займет длительное время
            FlushTimer.Stop();
            //слив служебных таблиц
            FlushMinorTables();            
            //создаем новый массив по объему кэша и копируем туда
            string[] ToSQL = new string[lSyslogCache[OldUsedCache].CacheIndex];
            Array.Copy(lSyslogCache[OldUsedCache].Cache, ToSQL, lSyslogCache[OldUsedCache].CacheIndex);
            //обнуляем индекс кэша
            lSyslogCache[OldUsedCache].ResetCacheIndex();
            //создаем новый поток и сливаев кэш в нем
            Thread FlushThread = new Thread(delegate() { db.ExecuteQueryBatch(ToSQL); });
            FlushThread.Start();
            //запускаем таймер обратно
            FlushTimer.Start();
            //снимаем отметку о сливе
            lSyslogCache[CurrentCache].Flushing = false;
            if (LogHandler != null)
                LogHandler("Cache flushed: "+ToSQL.Length.ToString());
        }

        private void FlushMinorTables()
        {
            //flush ip addresses
            int RowsCount = dtIPAddresses.Rows.Count;
            //если есть новые
            if (IPAddressesSaved != RowsCount)
            {
                //создаем массив и записываем туда запросы тех записей, которых в БД нет
                string[] Queries = new string[RowsCount - IPAddressesSaved];
                for (int i = IPAddressesSaved; i < RowsCount; i++)
                {
                    Queries[i - IPAddressesSaved] =
                        "insert into TAddresses (AddressID, IPAddress) values ('" + dtIPAddresses.Rows[i]["AddressID"].ToString() +
                        "','" + dtIPAddresses.Rows[i]["IPAddress"].ToString() + "')";
                }
                //выполняем пакетный запрос
                db.ExecuteQueryBatch(Queries);
                //отмечаем, что кол-во сохранненых
                IPAddressesSaved = RowsCount;
                if (LogHandler != null)
                    LogHandler("IPAddresses flushed: " + Queries.Length.ToString());
            }


            //flush interfaces
            RowsCount = dtInterfaces.Rows.Count;
            if (InterfacesSaved != RowsCount)
            {
                string[] Queries = new string[RowsCount - InterfacesSaved];
                for (int i = InterfacesSaved; i < RowsCount; i++)
                {
                    Queries[i - InterfacesSaved] =
                        "insert into TInterfaces (InterfaceID, AddressID, Name) values ('" + dtInterfaces.Rows[i]["InterfaceID"].ToString() +
                        "',";
                    if (DBNull.Value.Equals(dtInterfaces.Rows[i]["AddressID"]))
                        Queries[i - InterfacesSaved] += " NULL";
                    else
                        Queries[i - InterfacesSaved] += "'" + dtInterfaces.Rows[i]["AddressID"].ToString() + "'";
                    Queries[i - InterfacesSaved] += ", '" + dtInterfaces.Rows[i]["Name"].ToString() + "')";
                }
                db.ExecuteQueryBatch(Queries);
                InterfacesSaved = RowsCount;
                if (LogHandler != null)
                    LogHandler("Interfaces flushed: " + Queries.Length.ToString());
            }

            //flush actions
            RowsCount = dtActions.Rows.Count;
            if (ActionsSaved != RowsCount)
            {
                string[] Queries = new string[RowsCount - ActionsSaved];
                for (int i = ActionsSaved; i < RowsCount; i++)
                {
                    Queries[i - ActionsSaved] =
                        "insert into tactions (ActionID, Name) values ('" + dtActions.Rows[i]["ActionID"].ToString() +
                        "','" + dtActions.Rows[i]["Name"].ToString() + "')";
                }
                db.ExecuteQueryBatch(Queries);
                ActionsSaved = RowsCount;
                if (LogHandler != null)
                    LogHandler("Actions flushed: " + Queries.Length.ToString());
            }

            //flush protos
            RowsCount = dtProtos.Rows.Count;
            if (ProtosSaved != RowsCount)
            {
                string[] Queries = new string[RowsCount - ProtosSaved];
                for (int i = ProtosSaved; i < RowsCount; i++)
                {
                    Queries[i - ProtosSaved] =
                        "insert into tprotos (protoid, Name) values ('" + dtProtos.Rows[i]["ProtoID"].ToString() +
                        "','" + dtProtos.Rows[i]["Name"].ToString() + "')";
                }
                db.ExecuteQueryBatch(Queries);
                if (LogHandler != null)
                    LogHandler("Protocols flushed: " + Queries.Length.ToString());
                ProtosSaved = RowsCount;
            }

        }

        private bool ParseSysLogMessage(string SysLogMsg, ref SysLogEvent Data)
        {
            int Priority; 
            int Facility;
            int Severity;
            DateTime DTStamp;
            Data = new SysLogEvent();
            int startmark;
            int endmark;

            string Msg = SysLogMsg;

            // часть priority должна быть в угловых скобках, если это не так, то формат сообщения неверен и выходим
            startmark = Msg.IndexOf('<');
            endmark = Msg.IndexOf('>');
            if ((startmark != -1) && (endmark != -1))
            {
                Priority = Convert.ToByte(Msg.Substring(startmark + 1, endmark - startmark - 1));
                Severity = Priority & 8;  //последние 5 бит
                Facility = Priority >> 3; //первые 3 бита
            }
            else
                return false;
            Data.Facility = Facility;
            Data.Severity = Severity;

            // временая отметка должна быть в квадратных скобках, если это не так, то формат сообщения неверен и выходим
            startmark = Msg.IndexOf('[');
            endmark = Msg.IndexOf(']');
            if ((startmark != -1) && (endmark != -1))
            {
                DTStamp = DateTime.Parse(Msg.Substring(startmark + 1, endmark - startmark - 1));
            }
            else
                return false;
            Msg = Msg.Substring(endmark + 1);

            Data.DTStamp = DTStamp;

            //интересующие нас параметры
            string Conn = string.Empty;
            string Connsrcip = string.Empty;
            string Conndestip = string.Empty;
            string Termsent = "0";
            string Origsent = "0";
            //string Event = string.Empty;
            string RecvIf = string.Empty;
            string DestIf = string.Empty;
            string IPDataLen = "0";
            string Action = string.Empty;
            string Proto = string.Empty;

            //разбиваем строку разделителем "пробел", получаем пары имя=значение
            string[] MsgArr = Msg.Split(' ');
            foreach (string MsgElem in MsgArr)
            {                
                if (MsgElem.IndexOf('=') == -1)
                    continue;
                //разбиваем пару на составляющие "имя" и "значение"
                string[] Detail = MsgElem.Split('=');
                #region switch parameter
                switch (Detail[0])
                {
                    case "conn":
                        {
                            Conn = Detail[1];
                            break;
                        }
                    case "srcip":
                    case "connsrcip":
                        {
                            Connsrcip = Detail[1];
                            break;
                        }
                    case "destip":
                    case "conndestip":
                        {
                            Conndestip = Detail[1];
                            break;
                        }
                    case "termsent":
                        {
                            Termsent = Detail[1];
                            break;
                        }
                    case "origsent":
                        {
                            Origsent = Detail[1];
                            break;
                        }
                    //case "event":
                    //    {
                    //        Event = Detail[1];
                    //        break;
                    //    }
                    case "connrecvif":
                    case "recvif":
                        {
                            RecvIf = Detail[1];
                            break;
                        }
                    case "conndestif":
                        {
                            DestIf = Detail[1];
                            break;
                        }
                    case "ipdatalen":
                        {
                            IPDataLen = Detail[1];
                            break;
                        }
                    case "action":
                        {
                            Action = Detail[1];
                            break;
                        }
                    case "connipproto":
                    case "ipproto":
                        {
                            Proto = Detail[1];
                            break;
                        }

                }
                #endregion switch parameter
            }
            //если сообщение не указывает, что сессия отброшена или закрыта, то она нам не интересна
            if ((Action != "close") && (Action != "drop"))
                return false;
            try
            {
                Data.Src = IPAddress.Parse(Connsrcip);
                Data.Dst = IPAddress.Parse(Conndestip);
                Data.Transferred = Convert.ToInt32(Origsent);
                Data.Received = Convert.ToInt32(Termsent);
                Data.Action = Action;
                Data.Recvif = RecvIf;
                Data.Destif = DestIf;
                Data.Proto = Proto;
                Data.IPDatalen = Convert.ToInt32(IPDataLen);
            }
            catch
            {
                return false;
            }
            return true;
        }

        //<134>[2011-02-28 17:07:27] FW: CONN: prio=1 id=00600005 rev=1 event=conn_close_natsat action=close rule=Allow_Admins_group conn=close connipproto=TCP connrecvif=lan connsrcip=192.168.0.92 connsrcport=7507 conndestif=wan1 conndestip=199.59.148.20 conndestport=443 connnewsrcip=83.219.6.86 connnewsrcport=61416 connnewdestip=199.59.148.20 connnewdestport=443 origsent=1824 termsent=3472
        //<132>[2011-04-14 12:19:47] FW: RULE: prio=3 id=06000051 rev=1 event=ruleset_drop_packet action=drop rule=Default_Rule recvif=wan1 srcip=95.158.0.252 destip=83.219.6.86 ipproto=UDP ipdatalen=75 srcport=36113 destport=12684 udptotlen=75
        //<134>[2011-04-14 12:19:44] FW: CONN: prio=1 id=00600002 rev=1 event=conn_close action=close rule=Allow_Http2lan conn=close connipproto=TCP connrecvif=wan1 connsrcip=66.249.66.49 connsrcport=58359 conndestif=lan conndestip=83.219.6.86 conndestport=80 origsent=633 termsent=4890

    }
}
